Skip to main content

How to Write Migrations for Builder

Migrating Tables/Columns

To start, we must run this command inside the migrator container.
php migrator make:migration --path=migrations/database/builder/{version} {migration_name} with proper version and migration name. To get the correct version, pull the latest master, check the latest version, and create a new one with greater value.

Example: latest version is 1.0.74, we must create 1.0.75. Migration name examples: if you change a table or column, the good name is create_table_name_table or update_table_name_column_name, or if the changes are related to layouts, just describe it in a few words, for instance, update_checkout_page_form_submit_buttons or fix_shopping_cart_duplicate_buttons.

The migration body always contains the up method; we don't need the down method, as migrations always go in one direction.

Note: Be extremely careful, as we can't roll back it if something is wrong with migration; always test multiple times with multiple possible values and cases.

Everyone is responsible for his written migration and must be very careful, as this is the crucial part, and if something is wrong with migration, the entire application can crash.

Let's start with easy migration, which is adding/updating columns, or adding tables.
For example, we are requested to add a new table in the builder, to start, we create a migration file with the command mentioned above and then write actual code.

Example:

public function up(): void
{
if (!Schema::hasTable('redirections')) {
Schema::create('redirections', function (Blueprint $table) {
$table->id();
$table->string('from', 255)->unique();
$table->string('to', 255)->index();
$table->smallInteger('type');
$table->smallInteger('status')->index();
$table->timestamps();
});
}
}

Column update example:

public function up(): void
{
if (Schema::getColumnType('legal\_docs', 'description') === Types::TEXT) {
Schema::table('legal\_docs', function (Blueprint $table) {
$table->longText('description')->change();
});
}

if (Schema::getColumnType('legal\_doc\_translations', 'description') === Types::TEXT) {
Schema::table('legal\_doc\_translations', function (Blueprint $table) {
$table->longText('description')->change();
});
}
}

Another case is when the entire structure of the table is going to be changed. First, we must get the old data, update the table structure, and then insert data to not lose any.

Example:

Refer to this file 2023_10_16_145339_update_scripts_and_scripts_translations_table.php  located in 1.0.72.

Layout Migration

Moving forward, let's focus on layout migration. We have draft_layouts and layouts(published), and we have contents for both in draft_layout_contents and layout_contents tables which contain the translatable part of widgets with hashes. Each layout widget has params and may or may not contain props which are translatable parts (content).

We can start with an easy change; for example, some parameters were changed in the layout JSON file; first of all, we must get all layout files using this trait's method, trait: InteractsWithWidgets method getProjectAllLayouts. It has one argument, which is project id, $projectId = config('currentProjectId');.

Then we start to loop over all paths and check if the content is not empty.

foreach ($projectLayoutPaths as $path) {
$content = Storage::get($path);
$schema = json\_decode($content, true);

if (empty($schema)) {
continue;
}
}

Let's suppose that we are requested to change the contactDetails widgets params discriminator to email. We must create a new method that will recursively iterate over all children; let's name it updateContactDetailsParams.

protected function updateContactDetailsParams(array &$schema): void
{
foreach ($decoded as &$item) {
if($item\['type'\] === 'contactDetails') {
$item\['params'\]\['discriminator'\] = 'email';
}

if (isset($item\['children'\])) {
$this->updateContactDetailsParams($item\['children'\]);
}
}
}

Then call the updateContactDetailsParams method here:

foreach ($projectLayoutPaths as $path) {
$content = Storage::get($path);
$schema = json\_decode($content, true);

if (empty($schema)) {
continue;
}
$this->updateContactDetailsParams($schema);
Storage::put($path, json\_encode($schema));
}

Storage::put($path, json_encode($schema)); Will persist changes into the given path. We can do optimizations, like adding a modified flag, and if modified, then persist to file. 

Let's consider a scenario when a new child is added to an existing one. This task involves some complexity. The important part is having both content and schema files already defined in the directory.

$newChildPaths = \[
'widgetSchema' => storage\_path('files/product\_list/new\_last\_child\_label\_schema.json'),
'widgetContents' => storage\_path('files/product\_list/new\_last\_child\_label\_contents.json')
\];

Here we have new methods trait: InteractsWithPages method: retrievePaths method2: getLayoutSchemas. The first method gets the draft and published path from the given path, and the second method gets the draft and published layout schemas.

The next method is trait: InteractsWithWidgets method: syncLayoutsWidgets. In this case, we want to add a child to the price widget. First, we must sync the draft and published layouts. What does this mean? There can be cases when a draft version is completely different from the published, so this sync checks if it is not different and then adds new schema and content with the same hashes for the draft and the published layouts. If there are differences, it creates new hashes for draft and published and persists content.

foreach ($this->getProjectLayouts($projectId) as $path) {
$paths = $this->retrievePaths($path);
$schemas = $this->getLayoutSchemas($paths);

if (empty($schemas)) {
continue;
}

$sortedWidgets = $this->syncLayoutsWidgets(
'price',
$schemas\['draft'\],
$schemas\['published'\],
$newChildPaths,
$defaultLanguageId
);
}

The details can be found in 2023_08_03_051044_update_price_widget_add_new_child.php
under 1.0.51 folder.

There are lots of cases like inserting a new widget in a concrete position (we use array_splice), changing some other params, or adding a new one, etc. All these examples can be found in the migrations.

Another helpful method is the trait: InteractsWithWidgets method: collectWidgetReferences which collects all widgets from the layout by their type, and then you can do modifications to that list.

Another example is when you need to migrate a specific widget that is appearing only on a specific page. To migrate, let's observe an example.
Here is another method trait: InteractsWithWidgets method:getPageLayoutPaths which gets only layouts of the current given page.

Example:

public function up(): void
{
$page = DB::table('pages')->where('alias', 'my-account')->whereNull('parent\_id');

if (!$page->exists()) {
return;
}

$projectId = config('currentProjectId');
$paths = $this->getPageLayoutPaths($page, $projectId);
$schemas = $this->getLayoutSchemas($paths);

if (empty($schemas)) {
return;
}
// Logic goes here
}

UI Elements Migration

For UI elements migration, we must get data from DB, ui_element_settings table content column. The data inside the content contains params, children, and other details. To migrate, we need to first get content, make the changes, and then update DB.

Example:

public function up() : void
{
$uiElementSettings = DB::table('ui\_element\_settings')->get(\['id', 'content'\]);

foreach ($uiElementSettings as $setting) {
$settingContent = $setting->content;

if (!$settingContent) {
continue;
}

$content = json\_decode($settingContent, true);
$this->convert($content); // The main logic

DB::table('ui\_element\_settings')
->where('id', $setting->id)
->update(\['content' => json\_encode($content)\]);
}
}

VariantsStyles Migration

Every params of widget contains variantsStyles and it may or may not contain data. If we are requested to change some styles in it, then after migration, we must do CSS generation. We must generate new CSS only if something changes in variantsStyles or the UiElements content, or it can be something that is related to CSS; this must be checked by the developer. CSS generation is just an HTTP call to the builder API that generates all CSS.

To do so, we have a method trait: InteractsWithPages method: generateLayoutCss.

Example:

To not write a big example here, the details can be found in this migration 2023_08_11_092907_remove_is_microelement_from_add_to_cart_buttons located under 1.0.62 version.

The important part is that at the end, we are calling $this->generateLayoutCss();

Test Migrations

To ensure your migration is working properly, you must run this command inside the migrator container php migrator go:migrate:project --id=1 --v=1.0.70 --name=builder.ucraft.dev. --id represents your proejct id, --v represents to what version you want upgrade, --name is you project name. After running this command, you must see something like this:

Then you can open VE to check if your changes were applied, or open DB and/or layouts folder and check manually that everything is correct (table schema, layout JSON, etc.). If something is not correct, you can do CTRL + Z in the layout file to bring back the old file, then re-run migration. If something is wrong with DB, you can fresh:seed and then try again.

Note: Write migrations in a way that after running multiple times same migration will not break data. To ensure this, you can delete the migration record from the migrations table from your project and re-run it again.